package com.md.util;

import com.md.base.exception.ServerException;
import com.md.value.enums.ErrorEnum;
import lombok.extern.slf4j.Slf4j;
import org.redisson.api.RLock;
import org.redisson.api.RReadWriteLock;
import org.redisson.api.RedissonClient;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.DefaultTypedTuple;
import org.springframework.data.redis.core.HashOperations;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.ValueOperations;
import org.springframework.data.redis.core.ZSetOperations;
import org.springframework.stereotype.Component;

import javax.annotation.PreDestroy;
import javax.annotation.Resource;
import java.util.Collection;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.TimeUnit;
import java.util.function.Supplier;

/**
 * @author 刘骄阳
 */
@Component
@ConditionalOnClass({RedisConnectionFactory.class})
@Slf4j
public class RedisUtil {

  @Resource(name = "customRedisTemplate")
  private RedisTemplate<String, String> redisTemplate;

  @Resource
  private RedissonClient redisson;

  /**
   * 根据key删除缓存
   */
  public void remove(final String... keys) {
    for (String key : keys) {
      if (Boolean.TRUE.equals(exists(key))) {
        redisTemplate.delete(key);
      }
    }
  }


  /**
   * 根据key判断缓存是否存在数据
   */
  public boolean exists(final String key) {
    return Boolean.TRUE.equals(redisTemplate.hasKey(key));
  }

  /**
   * 根据key获取value,没有返回null
   *
   * @param key key
   */
  public String get(final String key) {
    //先判断是否存在
    if (!exists(key)) {
      //不存在直接返回null
      return null;
    }
    ValueOperations<String, String> operations = redisTemplate.opsForValue();
    return operations.get(key);
  }


  /**
   * 写入缓存
   *
   * @param key   key
   * @param value value
   */
  public void set(final String key, final Object value) {
    try {
      if (value.getClass().isPrimitive() || value instanceof String) {
        redisTemplate.opsForValue().set(key, value.toString());
      } else {
        redisTemplate.opsForValue().set(key, StringUtil.objectToJson(value));
      }
    } catch (Exception e) {
      log.error("写入redis发生问题: {}", e.getMessage(), e);
      throw ServerException.of(ErrorEnum.REDIS_SET_ERROR);
    }
  }

  /**
   * 写入缓存,设置有效期
   *
   * @param key        key
   * @param value      value
   * @param expireTime 有效时间(秒)
   */
  public void set(final String key, final Object value, final Long expireTime) {
    try {
      set(key, value);
      redisTemplate.expire(key, expireTime, TimeUnit.SECONDS);
    } catch (Exception e) {
      e.printStackTrace();
    }
  }

  /**
   * 写入缓存,设置有效期
   *
   * @param key        key
   * @param value      value
   * @param timeUnit   时间单位
   * @param expireTime 有效时间
   */
  public void set(final String key, final Object value, final TimeUnit timeUnit, final Long expireTime) {
    try {
      set(key, value);
      redisTemplate.expire(key, expireTime, timeUnit);
    } catch (Exception e) {
      e.printStackTrace();
    }
  }

  /**
   * 写入有序集合
   * 存入的时候,会自动根据score/weight排序
   * 分值越小越靠前
   *
   * @param key    键
   * @param value  值
   * @param weight 权重
   */
  public void setZList(final String key, final Object value, final Integer weight) {
    try {
      ZSetOperations<String, String> zSetOperations = redisTemplate.opsForZSet();
      if (value.getClass().isPrimitive() || value instanceof String) {
        zSetOperations.add(key, value.toString(), weight);
      } else {
        zSetOperations.add(key, StringUtil.objectToJson(value), weight);
      }
    } catch (Exception e) {
      e.printStackTrace();
    }
  }

  /**
   * 写入有序集合
   * 存入的时候,会自动根据score/weight排序
   * 分值越小越靠前
   *
   * @param key 键
   * @param set set集合
   */
  public void setzList(final String key, final Collection<?> set) {
    try {
      ZSetOperations<String, String> zSetOperations = redisTemplate.opsForZSet();
      Set<ZSetOperations.TypedTuple<String>> tuples = new HashSet<>();
      set.forEach(value -> {
        ZSetOperations.TypedTuple<String> tuple0;
        if (value.getClass().isPrimitive() || value instanceof String) {
          tuple0 = new DefaultTypedTuple<>(value.toString(), 0D);
        } else {
          tuple0 = new DefaultTypedTuple<>(StringUtil.objectToJson(value), 0D);
        }
        tuples.add(tuple0);
      });
      zSetOperations.add(key, tuples);
    } catch (Exception e) {
      e.printStackTrace();
    }
  }

  /**
   * 根据key增加集合中指定值的权重
   * 分值越小越靠前
   *
   * @param key    键
   * @param value  值
   * @param weight 要增加的权重
   */
  public void addScore(final String key, final Object value, final Integer weight) {
    try {
      ZSetOperations<String, String> zSetOperations = redisTemplate.opsForZSet();
      if (value.getClass().isPrimitive() || value instanceof String) {
        zSetOperations.incrementScore(key, value.toString(), weight);
      } else {
        zSetOperations.incrementScore(key, StringUtil.objectToJson(value), weight);
      }
    } catch (Exception e) {
      e.printStackTrace();
    }
  }

  /**
   * 根据key获取集合中指定值的权重
   * 分值越小越靠前
   *
   * @param key   键
   * @param value 值
   */
  public Double score(final String key, final Object value) {
    Double score = 0D;
    try {
      ZSetOperations<String, String> zSetOperations = redisTemplate.opsForZSet();
      if (value.getClass().isPrimitive() || value instanceof String) {
        score = zSetOperations.score(key, value.toString());
      } else {
        score = zSetOperations.score(key, StringUtil.objectToJson(value));
      }
    } catch (Exception e) {
      e.printStackTrace();
    }
    return score;
  }

  /**
   * 获取有序集合里的内容
   * 分值越小越靠前
   * 根据键获取集合,从startIndex开始,到endIndex结束
   * startIndex<=index<=endIndex
   *
   * @param key        键
   * @param startIndex 开始下标
   * @param endIndex   结束下标
   * @return Set集合
   */
  public Set<String> getzList(final String key, final Integer startIndex, final Integer endIndex) {
    Set<String> result = null;
    try {
      ZSetOperations<String, String> zSetOperations = redisTemplate.opsForZSet();
      //这是根据元素下标来查询value，用于分页查询，用这种方式吧
      result = zSetOperations.range(key, startIndex, endIndex);
    } catch (Exception e) {
      e.printStackTrace();
    }
    return result;
  }

  /**
   * 根据key和value删除有序集合中的数据
   * 分值越小越靠前
   *
   * @param key   键
   * @param value 值
   */
  public void removeList(final String key, final Object... value) {
    try {
      ZSetOperations<String, String> zSetOperations = redisTemplate.opsForZSet();
      zSetOperations.remove(key, value);
    } catch (Exception e) {
      e.printStackTrace();
    }
  }

  /**
   * 删除集合中的数据,从startIndex开始,到endIndex结束
   * 分值越小越靠前
   * startIndex<=index<=endIndex
   *
   * @param key        键
   * @param startIndex 开始下标
   * @param endIndex   结束下标
   */
  public void removeListByIndex(final String key, final Integer startIndex, final Integer endIndex) {
    try {
      ZSetOperations<String, String> zSetOperations = redisTemplate.opsForZSet();
      zSetOperations.removeRange(key, startIndex, endIndex);
    } catch (Exception e) {
      e.printStackTrace();
    }
  }

  /**
   * 根据键获取有序集合里的数量
   * 分值越小越靠前
   *
   * @param key 键
   * @return 总数
   */
  public Long getzListCount(final String key) {
    Long result = 0L;
    try {
      ZSetOperations<String, String> zSetOperations = redisTemplate.opsForZSet();
      result = zSetOperations.zCard(key);
    } catch (Exception e) {
      e.printStackTrace();
    }
    return result;
  }

  /**
   * 以增量的形式存储在redis中,
   *
   * @param key key
   * @return key增量之后的值
   */
  public Number increment(final String key) {
    //以增量的形式存储进redis
    return redisTemplate.opsForValue().increment(key, 1);
  }

  /**
   * 以减量的形式存储在redis中,
   *
   * @param key key
   * @return key减量之后的值
   */
  public Number decrement(final String key) {
    return redisTemplate.opsForValue().increment(key, -1);
  }

  /**
   * 以增量的形式存储在redis中,
   *
   * @param key        key
   * @param expireTime 有效时间(秒)
   * @return key增量之后的值
   */
  public Number increment(final String key, final Long expireTime) {
    //以增量的形式存储进redis
    Number count = increment(key);
    redisTemplate.expire(key, expireTime, TimeUnit.SECONDS);
    return count;
  }

  /**
   * 添加hash类型的数据到redis中
   *
   * @param key     key
   * @param hashKey hash的key
   * @param value   存入的值
   */
  public void setHash(final String key, final String hashKey, final String value) {
    HashOperations<String, Object, Object> opsForHash = redisTemplate.opsForHash();
    opsForHash.put(key, hashKey, value);
  }

  /**
   * 添加hash类型的数据到redis中
   *
   * @param key key
   * @param map map集合
   */
  public void setHash(final String key, final Map<String, String> map) {
    HashOperations<String, Object, Object> opsForHash = redisTemplate.opsForHash();
    opsForHash.putAll(key, map);
  }

  /**
   * 从redis中根据key和HashKey读取数据
   *
   * @param key     key
   * @param hashKey hash的key
   */
  public String getHash(final String key, final String hashKey) {
    HashOperations<String, String, String> opsForHash = redisTemplate.opsForHash();
    return opsForHash.get(key, hashKey);
  }

  /**
   * 从redis中根据key读取数据
   *
   * @param key key
   */
  public Map<String, String> getHash(final String key) {
    HashOperations<String, String, String> opsForHash = redisTemplate.opsForHash();
    return opsForHash.entries(key);
  }

  /**
   * 从redis中根据key和HashKey删除数据
   *
   * @param key     key
   * @param hashKey hash的key
   */
  public void deleteHash(final String key, final Object... hashKey) {
    HashOperations<String, Object, Object> opsForHash = redisTemplate.opsForHash();
    opsForHash.delete(key, hashKey);
  }


  /**
   * 可重入锁
   * 如果负责储存这个分布式锁的Redisson节点宕机以后，而且这个锁正好处于锁住的状态时，
   * 这个锁会出现锁死的状态。为了避免这种情况的发生，Redisson内部提供了一个监控锁的看门狗，
   * 它的作用是在Redisson实例被关闭前，不断的延长锁的有效期。
   * 默认情况下，看门狗的检查锁的超时时间是30秒钟，也可以通过修改Config.lockWatchdogTimeout来另行指定。
   * <p>
   * 另外Redisson还通过加锁的方法提供了leaseTime的参数来指定加锁的时间。超过这个时间后锁便自动解开了。
   *
   * @param key 锁key
   * @return 锁
   */
  public RLock lock(final String key) {
    return redisson.getLock(key);
  }

  /**
   * 读写锁
   * 分布式可重入读写锁允许同时有多个读锁和一个写锁处于加锁状态。
   *
   * @param key 锁key
   * @return 锁
   */
  public RReadWriteLock readWriteLock(final String key) {
    return redisson.getReadWriteLock(key);
  }

  public <T> T lockWithRedisson(final String lockKey,
                                final Supplier<T> supplier,
                                final int waitTime,
                                final int leaseTime) {
    RLock lock = redisson.getLock(lockKey);
    try {
      boolean isLocked = lock.tryLock(waitTime, leaseTime, TimeUnit.SECONDS);
      if (!isLocked) {
        throw ServerException.build(ErrorEnum.REQUEST_LOCK_FAILED);
      }
      return supplier.get();
    } catch (InterruptedException e) {
      log.error("Lock error, details: {}", e.getMessage(), e);
      throw ServerException.build(ErrorEnum.REQUEST_LOCK_FAILED);
    } finally {
      if (lock.isLocked() && lock.isHeldByCurrentThread()) {
        lock.unlock();
      }

    }
  }


  /**
   * 如果不添加下面这个方法，那项目在关闭Tomcat的时候会有线程未关闭。导致Tomcat无法顺利关闭。(重点)
   * 被@PreConstruct修饰的方法会在服务器卸载Servlet的时候运行，并且只会被服务器调用一次，类似于Servlet的destroy()方法。
   * 被@PreConstruct修饰的方法会在destroy()方法之后运行，在Servlet被彻底卸载之前。
   */
  @PreDestroy
  private void close() {
    redisson.shutdown();
  }
}
